package com.webserver.http;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;

/**
 * 作者：JustOne
 * 微信：Andylau2020smile
 * 抖音：JustOne2018
 * 日期：2022/6/12 22:37
 */
////景景临时理解：URL是总的， URI是部份
public class MyHttpRequest {
    //1，与客户端连接相关的定义
    private Socket socket;//核心人物，靠MyCliendHandler传过来
    private InputStream is;

    //2，【请求行】相关信息的定义
    private String method;  //请求方式
    private String uri;     //资源路径
    private String protocol;//协议版本

    //url中如果有?就是有参数，例：/myweb/reg?username=andy&password=123&nickname=andy&age=19
    private String requestURI;  //url中的请求部分，即问号左边(刘景景称之为请求地址)
    private String requestPar;  //url中的参数部分，即问号右边(刘景景称之为参数条)
    private Map<String,String> parameterMap = new HashMap<>();//参数键值对

    //3，【消息头】相关信息的定义，都是键值对，用Map
    private Map<String, String> headersMap = new HashMap<>();

    //4，【消息正文】相关信息的定义（暂无）
    private byte[] content;//消息体(二进制数组)

    ////////构造方法用来初始化请求参数
    ////////必须有socket才能得到对应的输入流用于读取客户端发送的请求
    public MyHttpRequest(Socket socket) throws MyEmptyRequestException {
        try{
            this.socket = socket;
            this.is = socket.getInputStream();

            System.out.println("\n解析开始");
            parseRequestLine();//------------方法一：解析请求行
            parseHeaders();    //------------方法二：解析消息头
            parseContent();    //------------方法三：解析消息体
            System.out.println("解析结束\n\n");

        }catch(MyEmptyRequestException e){
            throw e;//空请求不处理，直接抛出
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    ////////方法一：解析请求行。解析后放到[请求方式、请求路径、协议版本]属性上去
    private void parseRequestLine() throws MyEmptyRequestException, IOException {
        System.out.println("---请求行解析开始");

        //调用自己写的readLine()方法，一次读一行。然后按空格劈开成数组分配到三属性上。
        //但有可能会下标越界，因为http协议允许客户发送空请求。此情况后面再处理。
        //消息行只有一行，读一行即可
        String line = readLine();
        //空请求，抛异常
        if("".equals(line)){
            throw new MyEmptyRequestException();
        }
        String[] arr = line.split("\\s");//按空格分切，写成" "也可以
        for(String s : arr){
            System.out.println("------请求行：" + s);
        }
        this.method = arr[0];
        this.uri = arr[1];
        this.protocol = arr[2];

        //进一步解析请求行(即解析请求当中的uri)
        parseUri(this.uri);

        System.out.println("---请求行解析结束\n");
    }

    ////////进一步解析请求行(即解析请求当中的uri)
    //因uri中有可能有参数，有可能没参数。有参数的就是请求功能，没参数就是请求资源。
    //有?就是有参数(要功能)，否则就是没有(要资源)。
    //如果不含参数：把url直接赋给requestURI
    //如果含有参数：把url按?劈开，左边赋给requestURI
    //也即是：有可能是[index.html] ，也有可能是[reg?]，也有可能是[reg?name=andy]
    //前两种情况只能折出1项，如果大于1项，说明有参数
    public void parseUri(String uri){
        System.out.println("    解析uri开始");

        String[] temp = uri.split("\\?");//先按?拆分，左边是路径，右边是参数
        //第一部分：资源路径，即 register.html 或  doRegister
        requestURI = temp[0];
        //第二部分：参数。按常理来说此时应该拆分成了两部分。但如果页面输入框没有name属性，请求就会以问号结尾，就拆不出两项。
        if(temp.length > 1){
            this.requestPar = temp[1];//把参数条拆分成键值对放进map
            parseParameter(this.requestPar);
        }
        System.out.println("    解析uri结束");
    }


    ////////方法二：解析消息头。解析后放到map集合中
    private void parseHeaders() throws IOException {
        System.out.println("---消息头解析开始");
        int count = 0;
        while(true){
            String line = readLine();
            if("".equals(line)){
                break;
            }
            count++;
            System.out.println("------消息头" + count + ": "+line);
            String[] arr = line.split(": ");//英文的冒号 + 空格
            headersMap.put(arr[0], arr[1]);
        }

        System.out.println("---消息头解析结束\n");
    }

    ////////方法三：解析消息体
    private void parseContent() throws IOException {
        System.out.println("---消息体解析开始");

        //判断是否有消息正文，依据是消息头中是否有[Content-Length]
        //之前学过：判断map中是否包含给定的key或value
        //当时例子：boolean ck = map.containsKey("数学");//map中是否包含"数学"这个key
        if(headersMap.containsKey("Content-Length")){
            System.out.println("有正文");
            String lenStr = headersMap.get("Content-Length");//消息正文长度Str
            int lenNum = Integer.parseInt(lenStr);//-----------消息正文长度Num
            content = new byte[lenNum];//----------------------定义消息正文长度的数组
            is.read(content);//--------------------------------读取消息正文

            String contentType = headersMap.get("Content-Type");//根据消息头的提示分析正文类型
            //>>>>重要：是否是以form表单提交？
            //>>>>如果是，Content—Type的值就一定等于【application/x-www-form-urlencoded】
            if("application/x-www-form-urlencoded".equals(contentType)){
                try {
                    String requestPar = new String(content,"ISO8859-1");//正文就是参数条
                    System.out.println("正文中的参数条是：" + requestPar);
                    parseParameter(requestPar);//把参数条解析成键值对放进map
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
            }
            //else if("".equelse(contentType)){ 判断其他类型的正文类型 }
        }

        System.out.println("---消息体解析结束");
    }

    ////////进一步解析请求行(即解析请求当中的参数条)
    private void parseParameter(String str){
        //参数中可能有中文，已被转成了%XX的格式，我们需要转成回成中文，它只转带%的
        //username=%E5%88%98%E6%99%AF%E6%99%AF&password=123&nickname=JustOne&age=22
        //username=刘景景&password=123&nickname=JustOne&age=22
        try {
            str  = URLDecoder.decode(str,"utf-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        String[] kvArr = str.split("&");//先按&切成一对对的
        for(String s : kvArr){
            System.out.println("参数有：" + s);
            String[] kv = s.split("=");//再按=切成键值对
            String k = kv[0];
            String v = kv.length>=2 ? kv[1] : "";//如果用户没有填就只能折到一项
            parameterMap.put(k,v);
        }
    }


    //------------

    ////////自己写的读行方法
    //以CRLF(即回车换行)作为一行结束的标志。。
    //详解：65代表A，13代表回车(代码是CR)，10代表换行(代码是LF)，若有不明，请看《My020_API_v01/ pack01/ ASCII码》
    //异常请抛出，最终到达MyCliendHandler的run()
    public String readLine() throws IOException {
        //频繁进行字符串的拼接操作，所以用StringBuiler
        //之前学过：StringBuilder非线程安全，但相较于StringBuffer有速度优势，所以多数情况下建议使用 StringBuilder 类。
        StringBuilder str = new StringBuilder();
        int d = -1;
        char c1 = 'a';//相邻相两字符中的第一个
        char c2 = 'a';//相邻相两字符中的第二个
        while ( (d=is.read()) != -1 ){
            c2 = (char)d;
            if(c1==13 && c2==10){  //65代表A，13代表回车(代码是CR)，10代表换行(代码是LF)
                break;
            }
            str.append(c2);
            c1 = c2;//其实在不停的执行 c1=c2, c2 = 新
        }

        return str.toString().trim();
    }

    //--------------------------------------------------------------------
    //一些get方法，用于让外界获取这些解析好的信息

    ////////请求行的三个属性
    public String getMethod(){
        return this.method;
    }
    public String getUri() {
        return uri;
    }
    public String getProtocol() {
        return protocol;
    }

    ////////消息头的属性。不要把map提供出去了，因为别人可能会随意更改或清空。所以只提供"根据键查找值"的功能
    public String getHeadersValue(String youKey) {
        String myValue = headersMap.get(youKey);
        return myValue;
    }

    ////////获取路径，即请求行里面面？的左边
    public String getRequestURI(){
        return requestURI;
    }

    ////////通过 参数名，获取 参数值
    public String getParameter(String name){
        return parameterMap.get(name);
    }


}
